- Published on
- ·
React Query Key 효과적으로 관리하기
React Query
는 React 프로젝트에서 서버 상태 관리를 용이하게 해주는 강력한 라이브러리로 잘 알려져있습니다. 그 중 Query Key
(이하 쿼리 키)는 React Query에서 매우 중요한 개념입니다. 이 글은 쿼리 키의 특성과 어떻게 하면 이를 효과적으로 관리할 수 있는지, 그리고 실제 프로젝트에 적용시킨 사례에 대해 다룹니다.
캐싱 데이터
React Query는 같은 쿼리 키에 대해 다시 조회할 때 캐싱된 값을 받습니다. 이 쿼리 캐시는 직렬화된 key, value 쌍 자바스크립트 객체입니다. 그래서 우리는 이 캐시값을 가져오기 위해 쿼리 키
를 사용하는 것이고요. 키는 결정적인(deterministically) 방식으로 해싱되기 때문에 객체를 사용할 수도 있습니다.
useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... })
가장 중요한 것은 캐시에서 키에 대한 항목을 바로 찾기 위해, 쿼리는 고유한 키를 가져야 한다는 것입니다. 당연히 useQuery
와 useInfiniteQuery
에 동일한 키를 사용할 수 없습니다. 하나의 쿼리 캐시를 공유하게 될테니까 말이죠.
선언적 쿼리
쿼리와 재조회를 명령적인 방식으로 오해하기 쉽지만, 사실 선언적입니다. 쿼리는 자동으로 재조회됩니다. 우리가 할 일은 데이터를 다시 가져와야 하는 어떤 상태가 있다면 그것을 쿼리 키에 포함시키는 것 뿐입니다.
// 🚨 재조회를 직접 명령하는 것은 불가능합니다.
function Component() {
const { data, refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return <Filters onApply={() => refetch(???)} />
}
// ✅ 쿼리 키에 포함시켜 선언하면 자동으로 재조회합니다.
function Component() {
const [filters, setFilters] = React.useState()
const { data } = useQuery({
queryKey: ['todos', filters],
queryFn: () => fetchTodos(filters),
})
return <Filters onApply={setFilters} />
}
쿼리 키의 관리
이런 쿼리 키를 매번 수동으로 선언할 경우 오류가 발생하기 쉽고, 중복되기 쉬우며, 일관성이 없어집니다.
shiftList
를shitfList
로 작성하면 캐싱 되지 않습니다.- 이미 다른 곳에서 같은 목적의 쿼리 키를 만들었다면 과거의 쿼리 키를 찾아야 합니다.
- 이미 다른 곳에서 같은 목적의 쿼리 키를 만들었지만, 그것을 잊은채로 같은 쿼리를 다른 이름으로 명명할 수도 있습니다.
- 상수로 관리하기엔 키에 또 다른 세부 수준을 추가하기 어렵습니다.
쿼리 키 팩토리
각 기능이나 관심사마다 하나의 쿼리 키 팩토리
를 만들어서 이를 해결할 수 있습니다. 쿼리 키 팩토리는 단순한 객체로, 메서드를 통해 쿼리 키를 생성할 수 있습니다.
기본 구조 만들기
저희 프로젝트에서 ward
에 관련된 쿼리 키를 쿼리 키 팩토리를 통해 관리하는 기본 구조를 만들어보겠습니다.
export const wardKeys = {
base: ['ward'] as const,
requestList: (wardId: number, teamId: number, year: number, month: number) =>
[...wardKeys.base, 'requests', wardId, teamId, year, month] as const,
shiftList: (wardId: number, teamId: number, year: number, month: number) =>
[...wardKeys.base, 'shifts', wardId, teamId, year, month] as const,
linkedMembers: (wardId: number, teamId: number) =>
[...wardKeys.base, 'linked', wardId, teamId] as const,
};
매개변수 객체로 변경하기
추후에 매개변수가 늘어날 가능성을 고려하여, 객체 형태로 매개변수를 받는 것을 고려할 수 있습니다.
requestList: ({ wardId, teamId, year, month }: { wardId: number; teamId: number; year: number; month: number; }) =>
[...wardKeys.base, 'requests', wardId, teamId, year, month] as const,
이렇게 하면, 함수 호출 시에 매개변수의 순서를 신경 쓸 필요가 없어지고, 추후에 매개변수가 추가되거나 변경될 때 더 유연하게 대응할 수 있습니다.
JSDoc으로 queryFn 알려주기
조금만 더 신경써서, 쿼리 키 마다 JSDoc
을 사용해서 어떤 queryFn
과 매칭되는지 알려주기로 했습니다. 재사용할 때 해당 쿼리 키가 어떤 역할을 하는지 좀 더 분명해집니다.
/** getWardRequestList */
requestList: ({ wardId, teamId, year, month }: { wardId: number; teamId: number; year: number; month: number; }) =>
[...wardKeys.base, 'requests', wardId, teamId, year, month] as const,
쿼리 키 사용하기
const { data, isLoading } = useQuery(
wardKeys.requestList({wardId, teamId, year, month}),
() => getWardShiftRequest({wardId, teamId, year, month})
);
쿼리 키 팩토리 방식의 장점
- 쿼리 키를 중복 생성할 가능성을 예방합니다.
- 쿼리 키를 사용할 때 IDE에서 자동완성을 지원해주어, 오류를 방지합니다.
- 기존에 생성했던 쿼리 키를 찾기 위해 시간을 사용할 필요가 없습니다.
- 일정한 규칙을 따르고 있기 때문에 확장이 편합니다.
- 매개변수 타입을 명확이 정하여 타입 안정성을 보장합니다.
모든 상황에서 쿼리 키 팩토리를 써야하는 것은 아닙니다. 프로젝트 규모가 작거나, 이미 쿼리 키를 효과적으로 관리하고 있다면 굳이 쿼리 키 팩토리를 도입할 필요는 없습니다. 이 방법은 쿼리 키를 관리하는 여러 방법 중 React Query 라이브러리의 Maintainer TkDodo가 추천하는 방법입니다. 매개변수를 처리하는 방식이나 JSDoc을 사용하는 방식은 이것을 저의 프로젝트에 적용하면서 추가한 방식입니다.